using System;
using System.CodeDom;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Web.Compilation;

namespace AspNetExtensions.ExpressionBuilders {
  [ExpressionPrefix("ContextItem")]
  public class ContextObjectExpressionBuilder : ExpressionBuilder {

    // Parse the expression's arguments
    public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context) {
      return expression;
    }

    public override CodeExpression GetCodeExpression(
        System.Web.UI.BoundPropertyEntry entry,
        object parsedData,
        ExpressionBuilderContext context) {
      // Grab the name of the contextObject property
      if (!(parsedData is string))
        return null;

      string property = ((string)parsedData).Trim();

      // Must generate code for the right-side of an assignment
      // We make it call into a static method of this class

      // Define the CodeDOM object that represents the
      // parameter of the static method
      CodePrimitiveExpression prim = new CodePrimitiveExpression(property);
      CodePropertyReferenceExpression contextRef =
          new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "Context.Items");
      CodeExpression[] args = new CodeExpression[2] { prim, contextRef };

      // Define the CodeDOM object to represent the method invocation
      CodeTypeReferenceExpression refType;
      refType = new CodeTypeReferenceExpression(base.GetType());
      return new CodeMethodInvokeExpression(refType, "GetProperty", args);
    }

    public static object GetProperty(string propertyName, IDictionary contextObject) {
      // spllit the property using dot notation
      string[] propertyStrings = propertyName.Split('.');
      // Create a queue to keep track of the properties
      Queue<string> propertyStack = new Queue<string>(propertyStrings);

      // The first item referenced is the key in the dictionary
      string keyName = propertyStack.Dequeue();
      object keyObject = null;
      if (contextObject.Contains(keyName)) {
        keyObject = contextObject[keyName];
        if (propertyStack.Count > 0) {
          keyObject = GetPropertyObject(keyObject, propertyStack);
        }
      }
      return keyObject;
    }

    public static object GetPropertyObject(object o, Queue<string> propertyNames) {
      if (propertyNames.Count == 0)
        return null;

      string propertyName = propertyNames.Dequeue();
      object propertyObject = null;

      // Get the object reference by the first property on the stack
      // .. First find out if the object has a property with the first name on the stack
      Type objectType = o.GetType();
      PropertyInfo propInfo = objectType.GetProperty(propertyName);
      if (propInfo != null) {
        propertyObject = propInfo.GetValue(o, null);
        goto GetChildProperties;
      }

      // .. Next find out if the object is a dictionary object, and get the object with that key
      if (o is IDictionary) {
        IDictionary dict = (IDictionary)o;
        if (dict.Contains(propertyName)) {
          propertyObject = dict[propertyName];
          goto GetChildProperties;
        }
      }

      // Next find out if there is a non-void method with no parameters which has the same name
      MethodInfo method = objectType.GetMethod(propertyName, Type.EmptyTypes);
      if (method != null && method.ReturnType != typeof(void)) {
        propertyObject = method.Invoke(o, null);
        goto GetChildProperties;
      }

      // If the property name is an integer, it could be an index to a list
      int index;
      if (Int32.TryParse(propertyName, out index)) {
        if (o is IList) {
          IList propertyList = (IList)o;
          if (propertyList.Count > index) {
            propertyObject = propertyList[index];
            goto GetChildProperties;
          }
        }
      }

      // If we fell through to here, the property isn't discoverable
      return null;

        // If we were sent to this Goto label, the object was found. It could have child properties
    // if the propertyNames stack still has entries
    GetChildProperties:
      // If there are more properties on the stack, recursively get the object
      if (propertyNames.Count == 0)
        return propertyObject;
      else
        return GetPropertyObject(propertyObject, propertyNames);
    }
  }
}
